《Go 专家编程》笔记

Go 专家编程是一本比较通熟易懂的 Go 进阶书籍,早豆瓣评分也很高,学习比较整理如下,源码: https://github.com/rexyan/go_zjbc

Slice 切片

创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func main() {
// 变量声明, 值为 nil,长度为 0
var s []int

// 字面量
s1 := []int{} // 空切片,长度为0,值不是 nil,如果需要创建长度为 0 的切片,推荐变量声明的方式
s2 := []int{1, 2, 3} //长度为 3 的切片
fmt.Println(s, s1, s2) // [] [] [1 2 3]

// 内置函数
s3 := make([]int, 10) //指定长度
s4 := make([]int, 10, 100) // 指定长度和空间
fmt.Println(s3, s4) // [0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0]

// 切取
array := [5]int{1, 2, 3, 4, 5} // 创建数组
s5 := array[0:2] // 从数组中切取
s6 := s5[0:1] // 从切片中切取
fmt.Println(s5, s6) // [1 2] [1]

}

追加操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
s7 := make([]int, 0)
s7 = append(s7, 1) // 追加一个元素
s7 = append(s7, 2, 3, 4, 5, 6) // 追加多个
s7 = append(s7, []int{7, 8, 9}...) // 追加一个切片
fmt.Println(s7) //[1 2 3 4 5 6 7 8 9]

/*
当空间不足时,append会先创建新的大容量切片,添加元素后再返回新的切片
切片本身是结构体,结构体中直接存储了切片的长度和容量,所以获取这两个操作的时间复杂度都是 O(1)
因为切片本身是结构体,所以通过函数传递时,不会拷贝整个切片
*/
}

实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
/*
slice 数据结构如下:

type slice struct {
array unsafe.Pointer
len int
cap int
}
array 指向底层数组
len表示切片长度
cap表示数组容量
*/

array := [10]int{}
slice := array[5:7]
fmt.Println(len(slice), cap(slice)) // 2 5
// 切片从数组 array[5] 开始,到 array[7](不包含) 结束,长度为2,数组后面的内容都作为预留内存,即 cap 为 5
}

扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
slice := make([]int, 5, 5)
fmt.Println(len(slice), cap(slice)) // 5 5

// 追加一个元素后空间不够,触发扩容
slice = append(slice, 1)
fmt.Println(len(slice), cap(slice)) // 6 10

/*
扩容规则:
1.如果原 slice 容量小于 1024,则新的 slice 扩大为原来的 2 倍
2.如果原 slice 容量大于或等于 1024,则新的 slice 扩大为原来的 1.25 倍

append 添加元素实现步骤:
1. 假如Slice容量够用,则将新元素追加进去,Slice.len++,返回原Slice
2. 原Slice容量不够,则将Slice先扩容,扩容后得到新Slice
3. 将新元素追加进新Slice,Slice.len++,返回新的Slice。
*/
}

切片表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import "fmt"

func main() {
// 切片的简单表达式为 a[low: high]
/*
如果简单表达式切取的对象为字符串或数组,那么 low 和 high 应该满足一下关系: 0 <= low <= high <= len(a)
而,如果简单表达式切取的对象为切片,那么 low 和 high 的最大值可取 a 的容量,而不是 a 的长度:0 <= low <= high <= cap(a)

注意:作用于字符串的切片,得到的还是字符串。而不是切片。
a[:high] 等同于 a[0:high]
a[0:] 等同于 a[0:len(a)]
a[:] 等同于 a[0:len(a)]
*/
baseArray := [10]int{}
newSlice1 := baseArray[0:10] // 这里最多只能切到 10
fmt.Println(newSlice1, len(newSlice1), cap(newSlice1)) // [0 0 0 0 0 0 0 0 0 0] 10 10

baseSlice := make([]int, 0, 10) // 长度为0,容量为 10 的 slice
newSlice2 := baseSlice[2:5] // 这里切片的 low 和 high 可以超过 len(a)
fmt.Println(newSlice2, len(newSlice2), cap(newSlice2)) // [0 0 0] 3 8

// 切片的拓展表达式为 a[low: high: max], 使用 max 限制新生成切片的容量,新切片的容量为 max-low。它们需要满足以下关系:
// 0 <= low <= high <= max <= cap(a)
/*
对于切片的简单表达式会带来一些问题,例如:
a := [5]int{1,2,3,4,5}
b := a[1:4]
b = append(b, 0) // 此时元素a[4]将由5变为0
发生这种情况是 append 操作可能会覆盖 a[high] 及后面的元素。这是很危险的。所以推出了切片的拓展表达式为 a[low: high: max]
*/
array := [10]int{}
slice := array[5:7:7]
fmt.Println(slice, len(slice), cap(slice)) // [0 0] 2 2
// 如果使用简单表但是,那么上述代码中切片的容量将是5,而使用拓展表达式时,容量则是2。这时如果再次使用 append 进行追加, 如果空间不足则会产
// 生一个全新的切片,而不会覆盖原始的数组或切片。
}

String 字符串

注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
/*
1. string 可以为空(长度为0),但是不会是 nil
2. string 对象不可以修改。
3. string 和 []byte 的转换会发生一次内存拷贝,会有一定的开销
4. 使用的 + 进行字符串的拼接会触发内存的分配和拷贝。在拼接时会先计算最终字符串的长度后再次分配内存。
5. 字符串长度是指 Unicode 编码所占的字节数。
6. 使用 for-range 遍历字符串时,每次迭代将返回字符 UTF-8 编码的首个字节的下标及字节值。这意味着,下标可能不连续。
7. 字符串也可以使用反单引号表示。反单引号和双引号的区别是,反单引号是带有格式的。和 Python 中的 """""" 一致。
*/
s := "中国"
for index, value := range s {
fmt.Println(index, value)
/*
0 20013
3 22269
*/
}
}

Map

注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
// 未初始化的 map 默认值为 nil,向其中添加元素会触发 panic。
// 查询元素时,如果元素不存在,则会返回值类型的零值。
var exampleMap map[string]int
// exampleMap["1"] = 1 // 会触发 panic
// fmt.Println(exampleMap)

fmt.Println(exampleMap["key"]) // 0,返回值类型的零值
value, exist := exampleMap["key"] // 第二个参数返回 bool,表示 key 是否存在
if exist {
fmt.Println(value)
}

delete(exampleMap, "key1") // 不管 key 是否存在,或 map 是否已经初始化,进行 delete 操作时都不会报错。

// map 不能进行并发操作,如果需要并发读写,那么可以使用 sync 包中的 sync.Map。
}

实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

func main() {
/*
map 的数据结构如下所示:
type hmap struct {
count int // 当前保存的元素个数
B uint8 // 指示bucket数组的大小
buckets unsafe.Pointer // bucket数组指针,数组的大小为2^B
oldbuckets unsafe.Pointer // 老旧bucket数组指针,在扩容时会用到
...
}
*/

// map 的增删改查,在hash表中的操作:
/*
查找过程:
1. 跟据key值算出哈希值
2. 取哈希值低位与hmpa.B取模确定bucket位置
3. 取哈希值高位在tophash数组中查询
4. 如果tophash[i]中存储值也哈希值相等,则去找到该bucket中的key值进行比较
5. 当前bucket没有找到,则继续从下个overflow的bucket中查找。
6. 如果当前处于搬迁过程,则优先从oldbuckets查找
添加过程:
1. 跟据key值算出哈希值
2. 取哈希值低位与hmap.B取模确定bucket位置
3. 查找该key是否已经存在,如果存在则直接更新值
4. 如果没找到将key,将key插入
删除过程:
先在bucket中查找元素,存在则清除,否则什么也不做。
*/
}

iota

取值规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import "fmt"

func main() {
fmt.Println(LOG_EMERG) // 0
fmt.Println(LOG_ALERT) // 1
fmt.Println(LOG_CRIT) // 2
fmt.Println(LOG_ERR) // 3
fmt.Println(LOG_WARNING) // 4
fmt.Println(LOG_NOTICE) // 5
fmt.Println(LOG_INFO) // 6
fmt.Println(LOG_DEBUG) // 7

fmt.Println(bit0, mask0)
fmt.Println(bit1, mask1)
fmt.Println(bit3, mask3)

fmt.Println(mutexLocked)
fmt.Println(mutexWoken)
fmt.Println(mutexStarving)
fmt.Println(mutexWaiterShift)
fmt.Println(starvationThresholdNs)

}

type Priority int

/*
1. const 代表了 const 声明块的行索引(索引从 0 开始)
2. const 声明还有一个特点,如果为常量指定了一个表达式,但后续常量没有表达式,则继承上面的表达式
3. 单行 const 声明块中,没增加一行声明,iota 的值递增1,即便声明中没有使用 iota 也是如此
4. 单行声明语句中,即便多出现多个 iota,iota 的取值也保持不变
*/
const (
LOG_EMERG Priority = iota
LOG_ALERT
LOG_CRIT
LOG_ERR
LOG_WARNING
LOG_NOTICE
LOG_INFO
LOG_DEBUG
)

const (
// 左移:x<<y等于x乘以2^y,右移:则是除。

bit0, mask0 = 1 << iota, 1<<iota - 1 // const 声明第0行,iota 为0,所以 bit0=1,mask0=0
bit1, mask1 // const 声明第1行,iota 为1,且没有表达式,继承上面的表达式,所以 bit0=2,mask0=1
_, _ // const 声明第2行,iota 为2
bit3, mask3 // const 声明第3行,iota 为3,且没有表达式,继承上面的表达式,所以 bit0=8,mask0=7
)

const (
mutexLocked = 1 << iota // const 声明第0行,iota 为0,所以值为 1
mutexWoken // const 声明第1行,iota 为1,且没有表达式,继承上面的表达式,所以值为 2
mutexStarving // const 声明第2行,iota 为2,且没有表达式,继承上面的表达式,所以值为 4
mutexWaiterShift = iota // const 声明第3行,iota 为3
starvationThresholdNs = 1e6 // 科学计数法 1000000
)

Struct 结构体

获取 tag 信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"encoding/json"
"fmt"
"reflect"
)

type TypeMeta struct {
Kind string `json:"kind,omitempty"`
ApiVersion string `json:"apiVersion,omitempty"`
}

func main() {
t := TypeMeta{
Kind: "kind",
}
ty := reflect.TypeOf(t)

for i := 0; i < ty.NumField(); i++ {
fmt.Printf("字段名称:%s, Json Tag:%s\n", ty.Field(i).Name, ty.Field(i).Tag.Get("json"))
/*
字段名称:Kind, Json Tag:kind,omitempty
字段名称:ApiVersion, Json Tag:apiVersion,omitempty
*/
}
marshal, err := json.Marshal(t)
if err != nil {
return
}
fmt.Println(marshal)
}

Channel 通道

创建,以及触发nil情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
)

func main() {
// 变量方式声明管道,值为 nil,每个管道只能存储一种类型的数据 (对于 nil 的管道,无论读写都会阻塞,而且为永久阻塞)
var ch chan int
// 变量方式声明管道, 需要结合 make 才能进一步使用
ch = make(chan int, 8)

// 内置函数 make 声明
ch1 := make(chan string) // 不带缓存区
ch2 := make(chan string, 5) // 带缓存区

//<nil>
//0xc000102060
//0xc000126120
fmt.Println(ch)
fmt.Println(ch1)
fmt.Println(ch2)

/*
协程读取管道,会发生阻塞的情况:
1.管道无缓冲区
2.管道的缓冲区中无数据
3.管道的值为nil
协程写入管道,会发生阻塞的情况:
1.管道无缓冲区
2.管道的缓冲区已满
3.管道的值为nil
关闭管道,会发生阻塞的操作:
1.关闭值为nil的管道
2.关闭已经关闭的管道
3.向已经关闭的管道写入数据
*/
}

单向管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

/*
从管道的定义来看,并没有单向管道,所谓的单向管道,只是对管道的使用的一种限制来达到的目的。
*/

// 通过形参限制函数内部只能从管道中读取数据
func readChan(chanName <-chan int) {
<-chanName
}

// 通过形参限制函数内部只能从管道中写入数据
func writeChan(chanName chan<- int) {
chanName <- 1
}

func main() {
myChan := make(chan int, 10)
writeChan(myChan)
readChan(myChan)
}

select 和管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
"fmt"
"time"
)

func addNumberToChan(chanName chan int) {
for {
chanName <- 1
time.Sleep(time.Second * 1)
}

}

func main() {
var chan1 = make(chan int, 10)
var chan2 = make(chan int, 10)

go addNumberToChan(chan1)
go addNumberToChan(chan2)

for {
select {
case e := <-chan1:
fmt.Printf("从 chan1 接受到数据%d\n", e)
case e := <-chan2:
fmt.Printf("从 chan2 接受到数据%d\n", e)
default:
fmt.Printf("未接收到数据\n")
time.Sleep(time.Second * 1)
}
}
/*
从 chan2 接受到数据1
从 chan1 接受到数据1
从 chan2 接受到数据1
从 chan1 接受到数据1
未接收到数据
未接收到数据
从 chan2 接受到数据1
从 chan1 接受到数据1
从 chan1 接受到数据1
从 chan2 接受到数据1
未接收到数据
未接收到数据

以上结果可以看出,select 从管道中读取数据是随机的 (这是 select case 的特性,执行顺序就是随机的)
select 的 case 语句在读取管道时,尽管管道没有中没有数据也不会被阻塞。
*/
}

for-range 和管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"time"
)

func addDataToChan(chanName chan int) {
for {
chanName <- 1
time.Sleep(time.Second * 1)
}

}
func main() {
var chan1 = make(chan int, 10)
go addDataToChan(chan1)

// 持续的从管道中读取数据,当管道中没有数据时,当前协程会被阻塞
for e := range chan1 {
fmt.Printf("从 chan2 接受到数据%d\n", e)
}
}

Select

特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import "fmt"

func main() {
/*
select 只能作用于管道,包括数据的读取和写入。

第一种情况:
c := make(chan string)
SelectForChan(c)
此时管道无缓冲区,既不能写入,也不能读取,两个case均不执行,所以select陷入阻塞。

第二种情况:
c := make(chan string, 1)
SelectForChan(c)
此时管道有缓冲区,但是里面无数据,只能写入,写操作的 case得到执行,且执行后退出函数。

第三种情况:
c := make(chan string, 1)
SelectForChan(c)
此时管道有缓冲区,但是里面已满,只能读取数据,读操作的 case得到执行,且执行后退出函数。

第四种情况:
c := make(chan string, 2)
SelectForChan(c)
此时管道有缓冲区,但是缓存区没有满,此时既可以写入数据,也可以读取数据。select 将随机挑选一个 case 执行,且执行后退出函数
*/
}

func SelectForChan(c chan string) {
var recv string
send := "hello"

select {
case recv = <-c:
fmt.Printf("从管道接收到了数据%s\n", recv)
case c <- send:
fmt.Printf("将数据%s送入了管道\n", send)
}
}

管道返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func main() {
c := make(chan string)
close(c)
SelectAssign(c)
}

func SelectAssign(c chan string) {
select {
case <-c:
fmt.Println("不接收返回值")
case d := <-c:
fmt.Printf("接收一个返回值 %s", d)
case d, ok := <-c:
if !ok {
fmt.Println("管道已经关闭")
break
}
fmt.Printf("接收两个返回值 %s", d)
}

}

/*
如上所示,创建了一个无缓冲区的 chan,但是将这个 chan 关闭之后,三个 case 均可能执行。
第二个和第三个 case 接收到的数据都为空,但是第三个 case 可以感知管道被关闭。
*/

default

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
/*
创建一个没有缓冲区的管道,读操作肯定是阻塞的,然后 select 含有 default 分支,select 将执行
default 分支然后退出。另外,default 能出现在任意位置,且每个 select 只能含有一个 default。
*/
c := make(chan string)

select {
default:
fmt.Println("default")
case <-c:
fmt.Println("读取数据")
}
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"fmt"
"time"
)

func main() {
// 1. 永久阻塞
// select {}

// 2. 限时等待
stopCh := make(chan struct{})
go func() {
// 需要控制超时时间的业务逻辑
doSomething()
// 业务逻辑正常执行完,通知管道stopCh,执行完成
stopCh <- struct{}{}
}()
// 启用超时控制
stopWithTimeOut := waitForStopOrTimeOut(stopCh, 3*time.Second)
select {
// 若为超时,正常结束,isTimeOut=false, 否则为true
case isTimeOut := <-stopWithTimeOut:
if isTimeOut {
fmt.Println("end timeout")
} else {
fmt.Println("end ok")
}
}
}

func waitForStopOrTimeOut(stopCh <-chan struct{}, timeout time.Duration) <-chan bool {
stopWithTimeOut := make(chan bool)
go func() {
select {
case <-stopCh:
// 若接收到业务逻辑正常结束的消息,则为自然结束
fmt.Println("自然结束")
stopWithTimeOut <- false
case <-time.After(timeout):
// 若timeout时间内,未接收到业务逻辑正常结束的消息,则为超时
// timeout时间后,time.After(timeout)可读取到管道信息
fmt.Println("超时")
stopWithTimeOut <- true
}
close(stopWithTimeOut)
}()
return stopWithTimeOut
}
func doSomething() {
time.Sleep(time.Second * 5)
}

并发

channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"time"
)

func Process(ch chan int) {
time.Sleep(time.Second * 2)
// 写入管道,代表协程结束
ch <- 1
}

func main() {
// 创建一个包含十个元素的切片,元素类型为 channel
channels := make([]chan int, 10)
for i := 0; i < 10; i++ {
channels[i] = make(chan int) // 切片中放入一个 channel
go Process(channels[i]) // 启动协程,传入一个管道用于通信
}

// 父协程等待所有子协程结束
for i, ch := range channels {
<-ch
fmt.Printf("channel %d end!\n", i)
}
}

/*
使用channel控制子协程的优点是实现简单,缺点是需要创建大量的协程时,就创建相同数量的channel。
而且对子协程继续派生出来的协程不方便控制。
*/

WaitGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"sync"
"time"
)

func main() {
var wg sync.WaitGroup

wg.Add(2) // 设置计数器,即 goroutine 的数量

go func() {
time.Sleep(time.Second * 3)
fmt.Println("goroutine 1 end!")
wg.Done() // 执行结束后 goroutine -1
}()

go func() {
time.Sleep(time.Second * 1)
fmt.Println("goroutine 2 end!")
wg.Done() // 执行结束后 goroutine -1
}()

wg.Wait() // 主 goroutine 阻塞,等待计数器变为 0
fmt.Println("all goroutine finished!")
}

context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

// context 和 WaitGroup 最大的区别就是 context 对于派生的 goroutine 有更强的控制力。可以控制多级的 goroutine。
func main() {
/*
1. context 只定义了接口,凡是实现该接口的类都可以称为是一种 context。

type Context interface {
此方法返回一个是否设置 deadline 的 bool 值
Deadline() (deadline time.Time, ok bool)

当 context 关闭后 Done 返回一个关闭的通道。当 context 未关闭时,Done 返回 nil。
Done() <-chan struct{}

返回 context 关闭的原因。
Err() error

根据 key 查询 map 中的 value
Value(key any) any
}

2. context 包中定义了一个公用的 emptyCtx 全局变量,名为 background。可以使用 context.BackGround() 获取。
3. context 包中提供了四个方法创建不同类型的 context,使用这四个方法时,如果没有 context,则都需要传入 background。

emptyCtx
context interface cancelCtx timerCtx
valueCtx

emptyCtx,cancelCtx,valueCtx 都继承 context 接口
*/

}

cancelCtx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
"context"
"fmt"
"time"
)

func HandelRequest(ctx context.Context) {
go WriteRedis(ctx)
go WriteDB(ctx)

for {
select {
case <-ctx.Done():
fmt.Println("HandelRequest Done!")
return
default:
fmt.Println("HandelRequest Running!")
time.Sleep(time.Second * 1)
}
}
}

func WriteDB(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("WriteDB Done!")
return
default:
fmt.Println("WriteDB Running!")
time.Sleep(time.Second * 5)
}
}
}

func WriteRedis(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("WriteRedis Done!")
return
default:
fmt.Println("WriteRedis Running!")
time.Sleep(time.Second * 3)
}
}
}

func main() {

// 创建一个 cancelCtx,返回第一个参数是上下文,第二个参数是 cancel 方法。
ctx, cancel := context.WithCancel(context.Background())

// 开启协程,此协程里面又回派生两个协程。并将 ctx 传入
go HandelRequest(ctx)

// 等待协程执行
time.Sleep(time.Second * 10)

// cancel 掉所有子协程
cancel()

// 等待所有协程结束
time.Sleep(time.Second * 3)
}

timerCtx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package main

import (
"context"
"fmt"
"time"
)

func HandelRequest(ctx context.Context) {
go WriteRedis(ctx)
go WriteDB(ctx)

for {
select {
case <-ctx.Done():
fmt.Println("HandelRequest Done!")
return
default:
fmt.Println("HandelRequest Running!")
time.Sleep(time.Second * 1)
}
}
}

func WriteDB(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("WriteDB Done!")
return
default:
fmt.Println("WriteDB Running!")
time.Sleep(time.Second * 5)
}
}
}

func WriteRedis(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("WriteRedis Done!")
return
default:
fmt.Println("WriteRedis Running!")
time.Sleep(time.Second * 3)
}
}
}

func main() {
/*
timerCtx 在 cancelCtx 的基础上添加了 deadline,标示自动 cancel 的最终时间。
而 timer 就是一个自动触发的定时器。由此衍生出 WithDeadline() 和 WithTimeout() 两个方法。

WithDeadline: 指定最后期限,比如 context 将于某时间点自动结束
WithTimeout: 指定最长成活时间,比如 context 将于 30s 后结束
*/

// 创建一个定时 cancel 的 context。到期时间为当前时间后的 20s。也会返回 cancel 方法。可以在到期前手动 cancel context
ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second*10))

// 创建一个定时 cancel 的 context。到期时间为当前时间后的 10s。也会返回 cancel 方法。可以在到期前手动 cancel context
// ctx, _ := context.WithTimeout(context.Background(), time.Second*10)

// 开启协程,此协程里面又回派生两个协程。并将 ctx 传入
go HandelRequest(ctx)

// 等待协程执行
time.Sleep(time.Second * 15)
}

valueCtx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"context"
"fmt"
"time"
)

func main() {
timeoutCtx, _ := context.WithTimeout(context.Background(), time.Second*3)

// WithValue 获取到一个 ctx。WithValue 的父 ctx 是 WithTimeout 获取的。这样整个协程能在指定时间内结束。否则不会结束,需要手工控制。
valueCtx := context.WithValue(timeoutCtx, "param", "rex")
go HandelRequest(valueCtx)

time.Sleep(time.Second * 5)
}

func HandelRequest(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("HandelRequest Done!")
return
default:
fmt.Println("HandelRequest Running! Get Context Param value: ", ctx.Value("param"))
time.Sleep(time.Second * 1)
}
}
}

/*
context 总结:
1. Context 仅仅是一个接口,根据不同的实现,可以衍生出不同类型的 context。
2. cancelCtx 实现了 context 接口,通过 WithChancel() 创建 cancelCtx 实例。
3. timerCtx 实现了 context 接口,通过 WithDeadline(),WithTimeout() 创建 timerCtx 实例。
4. valueCtx 实现了 context 接口,通过 WithValue() 创建 valueCtx 实例。
*/

反射

比较两个结构体变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
"reflect"
)

type Foo struct {
A int
B string
C interface{}
}

func main() {
/*
使用 "==" 可以比较两个结构体变量,但仅限于结构体成员为简单类型。
使用 "==" 可以比较两个空接口类型变量,但仅限于底层类型一致,且不包含诸如 slice,map 等不可比较的类型。
常常使用 reflect.DeepEqual 来比较两个结构体成员变量和两个空接口类型变量。
*/
f1 := Foo{
A: 1,
B: "1",
C: []int{1, 2, 3},
}

f2 := Foo{
A: 1,
B: "1",
C: []int{1, 2, 3},
}

// fmt.Println(f1.C == f2.C) 报错,不能进行比较。
fmt.Println(IsEqual(f1.C, f2.C))

}

func IsEqual(a, b interface{}) bool {
return reflect.DeepEqual(a, b)
}

反射定律

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
"reflect"
)

func main() {
/*
interface 有一个(value,type)对,而反射就是操纵这个 (value,type) 对的机制。
reflect 中提供了 reflect.Type 和 reflect.Value 两个类型,分别代表 interface 中的 type 和 value。
通过提供了两个方法来获取 interface 的 type 和 value。TypeOf() 和 ValueOf()。
我们称 reflect.Type 和 reflect.Value 两个类型为 interface 的反射对象。
*/

// 第一定律:反射可以将 interface 类型变量转换成为反射对象

var x float64 = 3.4
t := reflect.TypeOf(x)
fmt.Println("type: ", t) // float64

v := reflect.ValueOf(x)
fmt.Println("value: ", v) // 3.4

// 第二定律:反射可以将反射对象还原成 interface 对象
var A interface{}
A = 100

z := reflect.ValueOf(A)
B := z.Interface() // 将反射对象还原为 interface 对象
fmt.Println(A == B) // true

// 第三定律:反射对象可修改,value 值必须是可设置的。
var k float64 = 54
i := reflect.ValueOf(&k)
i.Elem().SetFloat(89.1)
fmt.Println(k) // 89.1
fmt.Println(i.Elem().Interface()) // 89.1
}

逃逸分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package main

import "fmt"

func main() {
/*
逃逸分析是指由编译器决定内存分配的位置:
1. 如果分配在栈中,则函数执行完成后可自动将内存回收。
2. 如果分配在堆中,则函数执行完成后需要交给 GC(垃圾回收)处理。

逃逸策略:
1. 如果函数外部没有引用,则优先放到栈中。(也可能放到堆中,比如栈空间不足的时候)
1. 如果函数外部有引用,则一定放到堆中。

总结:
1. 栈上分配内存比在堆中分配内存有更高的效率
2. 栈上分配内存不需要处理GC
3. 堆上分配的内存使用完毕会交给GC处理
4. 逃逸分析的目的是决定分配地址是栈还是堆
5. 逃逸分析在编译阶段完成

注意:
函数传递指针真的比传值效率更高吗?
答案是否定的,传递指针的确可以减少底层的复制,但是如果复制的数据很小,由于传递指针会产生逃逸,则可能会使用堆,可能会增加GC的负担
所以函数传递指针不一定比传值效率更高。
*/
// 逃逸场景1,指针逃逸
RegisterStudent("rex", 18)

// 逃逸场景2,栈空间不足发生逃逸
Slice()

// 逃逸场景3,动态类型逃逸
Demo()

// 闭包饮用对象逃逸
f := Fibonacci()
for i := 0; i < 10; i++ {
fmt.Printf("%d\n", f())
}
}

type Student struct {
Name string
Age int
}

func RegisterStudent(name string, age int) *Student {
s := new(Student)
s.Age = age
s.Name = name
/*
s为局部变量,其通过函数返回值返回,s本身是一个指针,指向的内存地址是堆。
go build -gcflags=-m

# go_zhuanjia/escape
./main.go:23:6: can inline RegisterStudent
./main.go:3:6: can inline main
./main.go:14:17: inlining call to RegisterStudent
./main.go:14:17: new(Student) does not escape
./main.go:23:22: leaking param: name
./main.go:24:10: new(Student) escapes to heap
*/
return s
}

func Slice() {
/*
切片空间太大发生逃逸
go build -gcflags=-m

# go_zhuanjia/escape
./main.go:45:6: can inline Slice
./main.go:3:6: can inline main
./main.go:17:7: inlining call to Slice
./main.go:26:6: can inline RegisterStudent
./main.go:17:7: make([]int, 10000, 10000) escapes to heap
./main.go:26:22: leaking param: name
./main.go:27:10: new(Student) escapes to heap
./main.go:47:11: make([]int, 10000, 10000) escapes to heap

*/
s := make([]int, 10000, 10000)
for index, _ := range s {
s[index] = index
}
}

func Demo() {
s := "demo"
/*
很多函数的参数类型都是 interface,编译期间很难确定其具体的类型,也会发生逃逸
*/
fmt.Println(s)
}

func Fibonacci() func() int {
/*
a b 本来是局部变量,但因为闭包的饮用,所以不得不将其二者放到堆中
*/
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}

timer & ticker

一次性定时器 Timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"time"
)

func main() {
// time.NewTimer(d) 创建一个 Timer,Timer 定义的数据结构如下:
/*
type Timer struct {
C <-chan Time
r runtimeTimer
}
Timer 对外只暴露了一个 channel,指定时间到了就会往该 channel 中写入系统时间。
根据此特性,有以下执行场景:1. 设置超时时间,2. 或者延迟执行某个方法
*/
// 1. 设置超时时间
ch := make(chan string, 1)
WaitChannel(ch)
time.Sleep(time.Second * 2)

// 2. 延迟执行某个方法
DelayFunc()
}

func WaitChannel(ch <-chan string) bool {
timer := time.NewTimer(time.Second * 1)

select {
case <-ch: // 等待业务channel 的数据,如果超过1s还没有收到,则执行下面的 case
timer.Stop()
return true
case <-timer.C: // 业务 channel 超过1s还没有收到,则执行
fmt.Println("WaitChannel timeout!")
return false
}
}

func DelayFunc() {
timer := time.NewTimer(time.Second * 3)
select {
case <-timer.C:
fmt.Println("延迟执行方法")
}
}

Timer 对外暴露的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"time"
)

func main() {
// 1. 创建定时器
timer := time.NewTimer(time.Second * 5) // NewTimer 会创建一个新的 Timer 交给系统协程监控。
fmt.Println(timer)

// 2. 停止计时器
timer.Stop() // 返回 true 则表示定时器超前停止,后续不会在发送事件。返回 false 则代表定时器超时。

// 3. 重置定时器
timer.Reset(time.Second * 5) // 重置计时器。其原理是先停止以前的,然后在新注册。使用时要注意,一般只有过期的定时器才能重置(没过期的也能,但肯能出现正在执行的问题)
}

简单接口创建 Timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"log"
"time"
)

// 前面的是标准的创建 Timer 的方式。还可以通过 time 中提供的简单的方法来创建 Timer。
func main() {
// After
log.Println(time.Now())
time.After(time.Second * 2)
log.Println(time.Now())

// AfterFunc
log.Println(time.Now())
time.AfterFunc(time.Second*2, func() {
log.Println("AfterFunc End!", time.Now())
})
time.Sleep(time.Second * 3) // 等待协程退出
}

Timer 实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

func main() {
/*
Timer 数据结构如下所示:
type Timer struct {
C <-chan Time
r runtimeTimer
}
C 是面向 Timer 用户的。r 是面向底层定时器实现的。

runtimeTimer 数据结构如下所示:
type runtimeTimer struct {
pp uintptr // 系统底层存储 runtimeTimer 数组的地址
when int64 // 当前定时器触发时间
period int64 // 当前定时器触发间隔。对于 Timer 来说是 0
f func(any, uintptr) // 定时器执行时触发的回调函数
arg any // 回调函数第一个参数
seq uintptr // 回调函数第二个参数,Timer 没有。
nextwhen int64
status uint32
}

创建 Timer 源码如下:
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1) // 创建 channel,空间为1
t := &Timer{
C: c, // 用户可见的 C,就是一个 channel
r: runtimeTimer{ // 用户不可见的r
when: when(d), // Timer 的执行时间
f: sendTime, // Timer 执行的回调函数
arg: c, // Timer 执行的回调函数的参数
},
}
startTimer(&t.r) // 启动 Timer
return t
}

停止 Timer 源码如下:
func (t *Timer) Stop() bool {
if t.r.f == nil {
panic("time: Stop called on uninitialized Timer")
}
return stopTimer(&t.r)
}

重置 Timer 源码如下:
func (t *Timer) Reset(d Duration) bool {
if t.r.f == nil {
panic("time: Reset called on uninitialized Timer")
}
w := when(d)
return resetTimer(&t.r, w)
}
*/
}

周期性定时器 Ticker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"time"
)

func main() {
// 1. 创建定时器
ticker := time.NewTicker(time.Second * 2)

// 2. 停止定时器
defer ticker.Stop() // Ticker 在使用完成之后必须要释放,否则会产生资源泄露。进而消耗 CPU 的资源。

count := 0
for range ticker.C {
fmt.Println("2s 一次")
count += 1
if count >= 5 {
break
}
}
}

Ticker 实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

func main() {
/*
Ticker 数据结构如下所示:
type Ticker struct {
C <-chan Time
r runtimeTimer
}
C 是面向 Timer 用户的。r 是面向底层定时器实现的。

runtimeTimer 数据结构如下所示:
type runtimeTimer struct {
pp uintptr // 系统底层存储 runtimeTimer 数组的地址
when int64 // 当前定时器触发时间
period int64 // 当前定时器触发间隔
f func(any, uintptr) // 定时器执行时触发的回调函数
arg any // 回调函数第一个参数
seq uintptr // 回调函数第二个参数
nextwhen int64
status uint32
}

创建 Ticker 源码如下:
func NewTicker(d Duration) *Ticker {
c := make(chan Time, 1) // 创建 channel,空间为1
t := &Ticker{
C: c, // 用户可见的 C,就是一个 channel
r: runtimeTimer{ // 用户不可见的r
when: when(d), // Ticker 的执行时间
period: int64(d), // 和 Timer 的区别就在这里,Ticker 有间隔。
f: sendTime, // Ticker 执行的回调函数
arg: c, // Ticker 执行的回调函数的参数
},
}
startTicker(&t.r) // 启动 Ticker
return t
}

停止 Ticker 源码如下:
func (t *Ticker) Stop() {
stopTimer(&t.r)
}

重置 Ticker 源码如下:
func (t *Ticker) Reset(d Duration) {
if d <= 0 {
panic("non-positive interval for Ticker.Reset")
}
if t.r.f == nil {
panic("time: Reset called on uninitialized Ticker")
}
modTimer(&t.r, when(d), int64(d), t.r.f, t.r.arg, t.r.seq)
}
*/
}

defer

使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

/**
defer 关键字只能作用域函数或者函数调用
**/
func main() {
/**
1. 后接一个匿名函数
defer func() {
fmt.Println("hello!")
}()

2. 后接一个函数调用
file, err := os.Open("name")
if err != nil {
return nil, err
}
defer file.Close()

3. 释放资源
m.mutex.Lock()
defer m.mutex.Unlock()

4. 流程控制
var wg sync.WaitGroup
defer wg.Wait()

5. 异常处理
defer func() {
recover() // recover 只能用于 defer 函数中
}()
**/
}

行为规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package main

/**
1。defer 定义的延迟函数参数在 defer 语句出现时就已经确定了
2。defer 定义的顺序与实际的执行顺序相反
3。return 操作不是原子操作,执行过程是:保存返回值(若有) -> 执行defer(若有) -> 执行 ret 跳转。
4。申请资源后立即使用 defer 关闭资源是一个好习惯。
5。编译器会把每个延迟函数编译为一个 _defer 实例暂存到 goroutine 数据结构中,待函数结束后再逐个取出执行。每个 defer 语句对应一个 _defer 实例,多个实例形成一个链表,保存到 goroutine 数据结构中。每次插入 _defer 实例均插入链表头部,函数执行结束再依次从头部取出,从而实现后进先出的效果。

**/
func main() {
/**
这里的打印结果还是0,原因是 defer 定义的延迟函数参数在 defer 语句出现时就已经确定了。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}

==============================
定义 defer 的函数(主函数) 可能有返回值,返回值可能有名称(具名返回值),也可能没有返回值(匿名返回值),延迟函数可能会影响返回值。
关键字 return 并不是一个原子操作,比如 return i,实际上分为两步执行。
1. 先将 i 值存入栈中作为返回值
2. 然后执行跳转。而 defer 的执行正是在跳转之前。
所以,defer 执行时还是有机会操作返回值的。例如:
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i
}
该函数的 return 语句可以拆分为下面两行:
result = i
return
而延迟函数正是在执行 return 之前运行的。即加入 defer 之后执行过程如下:
result = i
result++
return

============ 主函数拥有匿名返回值,返回字面值 ============
func foo() int {
var i int
defer func() {
i++
}()
return 10
}
上面的函数 return 函数直接把 10 写入栈作为返回值。延迟函数无法操作返回值,所以就无法影响返回值。


============ 主函数拥有匿名返回值,返回变量 ============
一个函数拥有一个匿名返回值,返回本地或者全局变量,这种情况下 defer 语句可以引用返回值,但是不会改变返回值。
例如,一个返回本地变量的函数如下:
func foo() int {
var i int
defer func() {
i++
}()
return i
}
对于匿名函数的返回值,可以假设有一个变量存储返回值,假设返回值变量为 anony,则上面的返回语句可以拆分为:
anony = i
i++
return
由于 i 是整型值,会将值赋给 anony,所以在 defer 语句中修改 i值,不会对函数返回值造成影响。


============ 主函数拥有具名返回值 ============
func foo() (ret int) {
defer func() {
ret++
}()
return 0
}
上面的函数拆解出来如下所示:
ret = 0
ret++
return
函数真正返回前,在 defer 中对返回值做了 +1 操作,所以函数最终返回 1
**/

}

panic

注意事项

1
2
3
4
5
6
7
8
9
10
package main

func main() {
/**
1. 如果产生了 panic,那么程序会转向执行 defer 函数,当前函数的 defer 函数执行完毕后继续处理上层函数的 defer。当所有 defer 处理完后,程序退出。
2. panic 不会处理其他协程中的 defer。

每个协程中都维护了一个 defer 链表,执行过程中每遇到一个 defer 语句都会创建一个 defer 实例并插入链表。函数退出时取出本函数创建的实例并执行。panic 发生时,实际上是把流程转向了这个 defer 链表,程序专注于消费链表中的 defer 函数,当链表中的 defer 函数被消费完,程序在退出。
**/
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package main

import (
"fmt"
"time"
)

func main() {
// PanicDemo1() // 输出 C,B,A。panic 会触发所有 defer函数,然后panic会被PanicDemo1函数捕获。
// PanicDemo2() // 输出 C,B,A, 1。panic 会触发所有 defer函数,然后将异常抛给上层调用函数,然后触发上层调用函数的 defer 函数。
// PanicDemo3() // 输出 C,B,A 后触发 panic。panic 只能触发当前协程的 defer 函数,当前协程的 defer 处理结束后,如果没有 recover,则会引发退出。
// PanicDemo4() // 输出 B,A。panic 支持嵌套,defer 中可以支持再次触发 panic。
}

func PanicDemo1() {
defer func() {
recover()
}()
foo1()
}

func foo1() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
panic("demo")
defer fmt.Println("D")
}

func PanicDemo2() {
defer func() {
recover()
}()
defer func() {
fmt.Println("1")
}()
foo2()
}

func foo2() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
panic("demo")
defer fmt.Println("D")
}

func PanicDemo3() {
defer func() {
fmt.Println("demo")
}()

go foo3()
time.Sleep(time.Second * 1)
}

func foo3() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
panic("demo")
defer fmt.Println("D")
}

func PanicDemo4() {
defer func() {
recover()
}()
defer fmt.Println("A")
defer func() {
fmt.Println("B")
panic("panic in defer")
fmt.Println("C")
}()
panic("panic")
fmt.Println("D")
}

recover

注意事项

1
2
3
4
5
6
7
8
9
10
package main

func main() {
/**
1. recover() 函数调用必须要位于 defer 函数中,且不能出现在另一个嵌套的函数中。
2. recover() 函数成功处理异常后,无法再次回到本函数发生 panic 的位置继续执行。
3. recover() 函数可以消除本函数产生或收到的 panic,上游函数感知不到 panic 的发生。
4. 当发生 panic 并用 recover() 恢复后。对于匿名返回值,函数将返回相应类型的零值。对于具名返回值,函数将返回当前已经存在的值。
**/
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package main

import "fmt"

func main() {
// RecoverDemo1() // A。panic 在 defer中被捕获并消除,panic 之后语句并不会执行。
// RecoverDemo2() // 发生 panic,打印错误堆栈。函数中的 recover() 并不能捕获 panic,所以函数最终发生 panic。
// RecoverDemo3() // A, C。函数panic之后依次执行 defer 函数,所以输出 A,C
// RecoverDemo4() // B。函数中的panic被 recover() 消除后,无法继续使用 recover() 捕获。所以最后输出 B。
// RecoverDemo5() // 0。匿名返回值,返回函数将返回相应类型的零值。

}

func RecoverDemo1() {
defer func() {
if err := recover(); err != nil {
fmt.Println("A")
}
}()
panic("demo")
fmt.Println("B")
}

func RecoverDemo2() {
defer func() {
func() {
if err := recover(); err != nil {
fmt.Println("A")
}
}()
}()
panic("demo")
fmt.Println("B")
}

func RecoverDemo3() {
defer func() {
fmt.Println("C")
}()
defer func() {
if err := recover(); err != nil {
fmt.Println("A")
}
}()
panic("demo")
fmt.Println("B")
}

func RecoverDemo4() {
defer func() {
if err := recover(); err != nil {
fmt.Println("A")
}
}()
defer func() {
if err := recover(); err != nil {
fmt.Println("B")
}
}()
panic("demo")
fmt.Println("C")
}

func RecoverDemo5() {
foo := func() int {
defer func() {
recover()
}()
panic("demo")
return 10
}
ret := foo()
fmt.Println(ret)
}

异常处理

error 的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"errors"
"fmt"
)

/**
1. error 是一种内建的接口类型,不需要 import 即可使用。
2. error 接口只声明了一个 Error() 方法,所以只要任何实现了该方法的结构体,都可以当作 error 来使用。
error 的实例代表了一种异常的状态。Error() 方法用于描述该异常状态。
**/
func main() {
// 3. 创建 error
// 方法1
errors.New("new error")
// 方法2
fmt.Errorf("file not found, file name: %s", "1.txt")
// 如果不需要格式化字符串,那么使用 errors.New 效率更高。
}

检查 error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"errors"
"fmt"
"os"
)

func main() {
err := errors.New("test error")
// 1. 最简单的检查,是与 nil 比较
if err != nil {
fmt.Println("error")
}

// 2. 与预定义的 err 进行比较
ErrPermission := errors.New("permission denied")
if err == ErrPermission {
fmt.Println("permission error")
}

// 3. 断言
if e, ok := err.(*os.PathError); ok {
fmt.Printf("PathError, operation: %s, path: %s. msg :%v", e.Op, e.Path, e.Err)
}
}

传递 error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"errors"
"fmt"
)

/**
传递 error:
1. 在 go 1.13 以前,使用 fmt.Errorf("permission error %v", err) 来传递 error。这种做法的弊端是会丢失以前的 error 信息。
2. 在 go 1.13 以后,新增了一个 error 类型,wrapError。wrapError 和 errorString 相比,还额外实现了 Unwrap() 接口。用于返回原始的 error 信息。
3. 在 go 1.13 以后,增强了 fmt.Errorf(),可以通过 %w 来创建 wrapError 类型的 error。
4. 在 go 1.13 以后,引入了 errors.Unwrap() 来拆解 wrapError
5. 在 go 1.13 以后,引入了 errors.ls() 来检测 error传递链中是否包含指定的错误值。
6. 在 go 1.13 以后,引入了 errors.As() 来检测 error传递链中是否包含指定的错误类型。
**/
func main() {
// 1. Errorf 源码
/**
func Errorf(format string, a ...any) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a) 解析格式,如果发现 %w 动词,且提供了合法的 error 参数,那么就将 error 放入到 p.wrappedErr 中
s := string(p.buf)
var err error
if p.wrappedErr == nil { 没有动词 %w,生成基础的 error
err = errors.New(s)
} else { 有动词 %w,生成 wrapError
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
**/

// 2. 示例, 判断是否实现了 Unwrap() 接口
err := errors.New("this is demo error")
err2 := fmt.Errorf("same context: %v", err)
if _, ok := err2.(interface{ Unwrap() error }); !ok {
fmt.Println("未实现 Unwrap() 接口")
}

err3 := fmt.Errorf("same context: %w", err)
if _, ok := err3.(interface{ Unwrap() error }); ok {
fmt.Println("实现了 Unwrap() 接口")
}
}

链式 error 使用注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"errors"
"fmt"
)

/**
在使用 fmt.Errorf("permission error %w", err) 来创建一个链式 error 时,需要注意以下问题:
1. 每次只能接收一个 %w
2. %w 只匹配 error 参数
**/
func main() {
err := errors.New("demo error")
// 这样不行,每次只能接收一个 %w
fmt.Errorf("permission error %w, %w", err, err)
// 这样也不行,%w 只匹配 error 参数
fmt.Errorf("permission error %w", "this is text content")
}

Unwrap Is As 的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"errors"
"fmt"
"os"
)

func main() {
// 如果把 error 比作一件衣服,fmt.Errorf("xxx", %w) 就好比给 error 增加了一件外套。而 Unwrap() 函数则是脱下外套。
// errors.Unwrap() 源码如下:
/**
func Unwrap(err error) error {
u, ok := err.(interface { 判断 err 是否实现了 Unwrap 方法
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
**/

// errors.Unwrap 使用示例
err := fmt.Errorf("write file error: %w", os.ErrPermission)
if errors.Unwrap(err) == os.ErrPermission {
fmt.Println("Permission denied")
}

// errors.ls 使用示例。
// errors.Is 用户判断 error 链中是否包含指定的 error 值。
err2 := fmt.Errorf("write file error: %w", os.ErrPermission)
err3 := fmt.Errorf("write file error: %w", err2)
if errors.Is(err3, os.ErrPermission) {
fmt.Println("Permission denied")
}

// errors.As 使用示例。
// errors.As 用户判断 error 链中是否有指定类型出现,如果有,则把 error 转换成该类型。

err4 := &os.PathError{
Op: "write",
Path: "/root/xxx",
Err: os.ErrPermission,
}
err5 := fmt.Errorf("some context: %w", err4)

var target *os.PathError
// 逐层剥离 err5,并检测 err5 是否是 PathError 类型。
if errors.As(err5, &target) {
fmt.Println("PathError")
}
}

测试

单元测试

1
2
3
4
5
6
7
8
9
10
11
12
package test

/**
编写单元测试时,应该遵守的规则:
1.测试文件名必须以 _test.go 结尾
2.测试函数名必须以 TestXxx 开始
3.在命令行下使用 go test 即可开启测试
**/

func Add(a int, b int) int {
return a + b
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package test

import "testing"

func TestAdd(t *testing.T) {
var a = 1
var b = 2
var c = 3

r := Add(a, b)
if c != r {
t.Error("test Add fail")
}
}

性能测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package test

/**
编写性能测试时,应该遵守的规则:
1.测试文件名必须以 _test.go 结尾
2.测试函数名必须以 BenchmarkXxx 开始
3.在命令行下使用 go test -bench=. 即可开启测试
*/

func MakeSliceWithoutAlloc() []int {
var newSlice []int
for i := 0; i < 100000; i++ {
newSlice = append(newSlice, i)
}
return newSlice
}

func MakeSliceWithAlloc() []int {
var newSlice []int
newSlice = make([]int, 100000)

for i := 0; i < 100000; i++ {
newSlice = append(newSlice, i)
}
return newSlice
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package test

import "testing"

func BenchmarkMakeSliceWithoutAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
MakeSliceWithoutAlloc()
}
}

func BenchmarkMakeSliceWithAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
MakeSliceWithAlloc()
}
}

示例测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package test

import "fmt"

/**
1.示例测试函数名需要以 Example 开头
2.检测单行输出格式为 // Output: 期望字符串
3.检测多行输出格式为 // Output: 期望字符串 \n 期望字符串 \n 期望字符串,每个期望字符串占一行
4.检测无序输出格式为 // Unordered output: 期望字符串 \n 期望字符串 \n 期望字符串,每个期望字符串占一行
5.测试字符串时会自动忽略前后的空格
6.如果测试函数中没有 Output 标识,则该测试函数不会被执行
7.执行测试函数时可以使用 go test,此时该目录下的其他测试文件也会一起执行,如果要单独执行,可以使用 go test 文件名。
*/

func SayHello() {
fmt.Println("Hello!")
}

func SayGoodBye() {
fmt.Println("Hello!")
fmt.Println("GoodBye!")
}

func PrintNames() {
names := make(map[int]string, 4)
names[1] = "Jim"
names[2] = "Bob"
names[3] = "Tom"
names[4] = "Sue"

for _, name := range names {
fmt.Println(name)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package test

func ExampleSayHello() {
SayHello()
// Output: Hello!
}

func ExampleSayGoodBye() {
SayGoodBye()
// Output:
// Hello!
// GoodBye!
}

func ExamplePrintNames() {
PrintNames()
// Unordered output:
// Jim
// Bob
// Sue
// Tom
}

子测试

普通子测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package test

import "testing"

func sub1(t *testing.T) {
var a = 1
var b = 2
var c = 3

r := Add(a, b)
if c != r {
t.Error("test Add fail")
}
}

func sub2(t *testing.T) {
var a = 1
var b = 2
var c = 3

r := Add(a, b)
if c != r {
t.Error("test Add fail")
}
}

func sub3(t *testing.T) {
var a = 1
var b = 2
var c = 3

r := Add(a, b)
if c != r {
t.Error("test Add fail")
}
}

func TestSub(t *testing.T) {
t.Run("sub1", sub1)
t.Run("sub2", sub2)
t.Run("sub3", sub3)
}

/**
=== RUN TestSub
=== RUN TestSub/sub1
=== RUN TestSub/sub2
=== RUN TestSub/sub3
--- PASS: TestSub (0.00s)
--- PASS: TestSub/sub1 (0.00s)
--- PASS: TestSub/sub2 (0.00s)
--- PASS: TestSub/sub3 (0.00s)
PASS
*/

并发子测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package test

import (
"testing"
"time"
)

func parallelTest1(t *testing.T) {
t.Parallel()
time.Sleep(3 * time.Second)
}

func parallelTest2(t *testing.T) {
t.Parallel()
time.Sleep(2 * time.Second)
}

func parallelTest3(t *testing.T) {
t.Parallel()
time.Sleep(1 * time.Second)
}

func TestSubParallel(t *testing.T) {
t.Run("group", func(t *testing.T) {
t.Run("Test1", parallelTest1)
t.Run("Test2", parallelTest2)
t.Run("Test3", parallelTest3)
})
}